import Compiler from "./Compiler";
import CompilerInterface from "./CompilerInterface";
import { is_null, empty, isset } from "../Support/Obj";
import * as Str from "../Support/Str";
import * as Arr from "../Support/Arr";
import { count } from "../Support/Arr";
import { join } from 'path'
import * as XRegExp from 'xregexp'
import { strpos } from "../Support/Str";

export class BladeCompiler extends Compiler implements CompilerInterface
{
    // use Concerns\CompilesComments,
    //     Concerns\CompilesComponents,
    //     Concerns\CompilesConditionals,
    //     Concerns\CompilesEchos,
    //     Concerns\CompilesIncludes,
    //     Concerns\CompilesInjections,
    //     Concerns\CompilesLayouts,
    //     Concerns\CompilesLoops,
    //     Concerns\CompilesRawPhp,
    //     Concerns\CompilesStacks,
    //     Concerns\CompilesTranslations;

    /**
     * All of the registered extensions.
     *
     * @var array
     */
    protected extensions: any[] = [];

    /**
     * All custom "directive" handlers.
     *
     * This was implemented as a more usable "extend" in 5.1.
     *
     * @var array
     */
    protected customDirectives: { [key: string]: any } = {};

    /**
     * The file currently being compiled.
     *
     * @var string
     */
    protected path!: string;

    /**
     * All of the available compiler functions.
     *
     * @var array
     */
    protected compilers = [
        'Comments',
        'Extensions',
        'Statements',
        'Echos',
    ];

    /**
     * Array of opening and closing tags for raw echos.
     *
     * @var array
     */
    protected rawTags = ['{!!', '!!}'];

    /**
     * Array of opening and closing tags for regular echos.
     *
     * @var array
     */
    protected contentTags = ['{{', '}}'];

    /**
     * Array of opening and closing tags for escaped echos.
     *
     * @var array
     */
    protected escapedTags = ['{{{', '}}}'];

    /**
     * The "regular" / legacy echo string format.
     *
     * @var string
     */
    protected echoFormat = 'e(%s)';

    /**
     * Array of footer lines to be added to template.
     *
     * @var array
     */
    protected footer = [];

    /**
     * Placeholder to temporary mark the position of verbatim blocks.
     *
     * @var string
     */
    protected verbatimPlaceholder = '@__verbatim__@';

    /**
     * Array to temporary store the verbatim blocks found in the template.
     *
     * @var array
     */
    protected verbatimBlocks :any[]= [];

    /**
     * Compile the view at the given path.
     *
     * @param  string  path
     * @return void
     */
    public async compile(path?: string)
    {
        if (path) {
            this.setPath(path);
        }

        if (! is_null(this.cachePath)) {
            const contents = this.compileString(await this.files.get(this.getPath()));

            return this.files.put(this.getCompiledPath(this.getPath()), contents);
        }
    }

    /**
     * Get the path currently being compiled.
     *
     * @return string
     */
    public  getPath()
    {
        return this.path;
    }

    /**
     * Set the path currently being compiled.
     *
     * @param  string  path
     * @return void
     */
    public  setPath(path: string)
    {
        this.path = path;
    }

    /**
     * Compile the given Blade template contents.
     *
     * @param  string  value
     * @return string
     */
    public  compileString(value: string)
    {
        let result: string;

        if (strpos(value, '@verbatim') !== false) {
            value = this.storeVerbatimBlocks(value);
        }

        this.footer = [];

        // Here we will loop through all of the tokens returned by the Zend lexer and
        // parse each one into the corresponding valid PHP. We will then have this
        // template as the correctly rendered PHP that can be rendered natively.
        result = this.parseTemplate(value);

        if (!empty(this.verbatimBlocks)) {
            result = this.restoreVerbatimBlocks(result);
        }

        // If there are any footer lines that need to get added to a template we will
        // add them here at the end of the template. This gets used mainly for the
        // template inheritance via the extends keyword that should be appended.
        if (count(this.footer) > 0) {
            result = this.addFooters(result);
        }

        return result;
    }

    /**
     * Store the verbatim blocks and replace them with a temporary placeholder.
     *
     * @param  string  value
     * @return string
     */
    public storeVerbatimBlocks(value: string)
    {
        const preg = XRegExp('(?<!@)@verbatim(.*?)@endverbatim', 'gms');
        return XRegExp.replace(value, preg, (match: string, p1: string) => {
            this.verbatimBlocks.push(p1);
            return this.verbatimPlaceholder;
        })
    }

    /**
     * Replace the raw placeholders with the original code stored in the raw blocks.
     *
     * @param  string  result
     * @return string
     */
    public restoreVerbatimBlocks(result: string)
    {
        const preg = XRegExp(this.verbatimPlaceholder, 'gms')
        const newResult = XRegExp.replace(result, preg, () => {
            return this.verbatimBlocks.shift()
        })
        this.verbatimBlocks = []

        return newResult
    }

    /**
     * Add the stored footers onto the given content.
     *
     * @param  string  result
     * @return string
     */
    protected addFooters(result: string)
    {
        return join(result, ...this.footer.reverse());
    }

    /**
     * Parse the tokens from the template.
     *
     * @param  {string} token
     * @return string
     */
    public parseTemplate(token: string)
    {
        let content = token
        let that = this as any

        for (let type of this.compilers) {
            content = that[`compile${type}`](content);
        }

        return content;
    }

    /**
     * Execute the user defined extensions.
     *
     * @param  string  value
     * @return string
     */
    protected compileExtensions(value: string)
    {
        for (let compiler of this.extensions) {
            value = compiler(value, this);
        }

        return value;
    }

    /**
     * Compile Blade statements that start with "@".
     *
     * @param  string  value
     * @return string
     */
    public compileStatements(value: string)
    {
        // const preg = XRegExp('\B@(@?\w+(?:::\w+)?)([ \t]*)(\( ( (?>[^()]+) | (?3) )* \))?', 'x')
        const preg = XRegExp(/\B@(@?\w+(?:::\w+)?)([ \t]*)(\(([^()]+)\))?/, 'x')
        return XRegExp.replace(value, preg, (...match: string[]) => {
            return this.compileStatement(match)
        })
    }

    /**
     * Compile a single Blade @ statement.
     *
     * @param  array  match
     * @return string
     */
    protected compileStatement(match: string[])
    {
        if (Str.contains(match[1], '@')) {
            match[0] = match[3] ? (match[1] + match[3]) : match[1];
        } else if (isset(this.customDirectives, match[1])) {
            match[0] = this.callCustomDirective(match[1], Arr.get(match, 3));
        } else if (isset(this, 'compile' + Str.ucFirst(match[1]))) {
            let that = this as any
            let method = 'compile' + Str.ucFirst(match[1])
            match[0] = that[method](Arr.get(match, 3));
        }

        return match[3] ? match[0] : (match[0] + match[2]);
    }

    /**
     * Call the given directive with the given value.
     *
     * @param  string  name
     * @param  string|null  value
     * @return string
     */
    protected callCustomDirective(name: string, value?: string)
    {
        // if (Str::startsWith(value, '(') && Str::endsWith(value, ')')) {
        //     value = Str::substr(value, 1, -1);
        // }

        // return call_user_func(this.customDirectives[name], trim(value));
        return ""
    }

    /**
     * Strip the parentheses from the given expression.
     *
     * @param  string  expression
     * @return string
     */
    public  stripParentheses(expression: string)
    {
        // if (Str::startsWith(expression, '(')) {
        //     expression = substr(expression, 1, -1);
        // }

        // return expression;
        return ""
    }

    /**
     * Register a custom Blade compiler.
     *
     * @param  callable  compiler
     * @return void
     */
    public extend(compiler: any)
    {
        this.extensions.push(compiler);
    }

    /**
     * Get the extensions used by the compiler.
     *
     * @return array
     */
    public  getExtensions()
    {
        return this.extensions;
    }

    /**
     * Register a handler for custom directives.
     *
     * @param  string  name
     * @param  callable  handler
     * @return void
     */
    public  directive(name: string, handler: any)
    {
        this.customDirectives[name] = handler;
    }

    /**
     * Get the list of custom directives.
     *
     * @return array
     */
    public  getCustomDirectives()
    {
        return this.customDirectives;
    }

    /**
     * Set the echo format to be used by the compiler.
     *
     * @param  string  format
     * @return void
     */
    public  setEchoFormat(format: string)
    {
        this.echoFormat = format;
    }
}
